/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package org.geotools.tutorial;

import DirectedGraph.DirectedDijkstraShortestPathFinder;
import DirectedGraph.MyFeatureGraphGenerator;
import Search.MySearch;
import java.util.Collection;
import org.geotools.geometry.DirectPosition2D;
import com.vividsolutions.jts.geom.Coordinate;
import java.util.Iterator;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureSource;
import org.geotools.filter.text.cql2.CQLException;
import org.geotools.swing.event.MapMouseEvent;
import org.opengis.feature.Feature;
import org.opengis.feature.simple.SimpleFeatureType;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.referencing.CRS;
import org.opengis.referencing.crs.CoordinateReferenceSystem;

import org.opengis.feature.simple.SimpleFeature;
import org.opengis.filter.Filter;
import org.opengis.filter.FilterFactory2;

import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.MultiLineString;
import java.awt.geom.Point2D;
import java.io.IOException;
import javax.measure.unit.Unit;
import org.geotools.data.DataUtilities;
import org.geotools.feature.FeatureIterator;
import org.geotools.filter.text.cql2.CQL;
import org.geotools.geometry.jts.JTS;
import org.geotools.geometry.jts.JTSFactoryFinder;
import org.geotools.graph.build.line.DirectedLineStringGraphGenerator;
import org.geotools.graph.path.Path;
import org.geotools.graph.structure.Edge;
import org.geotools.graph.structure.Node;
import org.geotools.graph.structure.basic.BasicDirectedGraph;
import org.geotools.graph.traverse.standard.DijkstraIterator;
import org.geotools.graph.traverse.standard.DirectedDijkstraIterator;
import org.opengis.geometry.DirectPosition;
import org.opengis.referencing.operation.MathTransform;

/**
 *
 * @author oleg
 */
public class Guide {

    private SimpleFeatureSource featureSource;
    //это узлы для определения направлений
    SimpleFeatureSource featureSourceNodes = null;
    CoordinateReferenceSystem oldCRS;
    CoordinateReferenceSystem newCRS;
    SimpleFeature featureStartOld;
    SimpleFeature featureFinishOld;
    SimpleFeature simpleFeatureStart[];
    SimpleFeature simpleFeatureFinish[];
    int add = 0;
    int searchType;

    public SimpleFeature[] getSimpleFeatureFinish() {
        return simpleFeatureFinish;
    }

    public SimpleFeature[] getSimpleFeatureStart() {
        return simpleFeatureStart;
    }

    public void setSimpleFeatureFinish(SimpleFeature[] simpleFeatureFinish) {
        this.simpleFeatureFinish = simpleFeatureFinish;
    }

    public void setSimpleFeatureStart(SimpleFeature[] simpleFeatureStart) {
        this.simpleFeatureStart = simpleFeatureStart;
    }

    public SimpleFeature getFeatureFinishOld() {
        return featureFinishOld;
    }

    public SimpleFeature getFeatureStartOld() {
        return featureStartOld;
    }

    public void setFeatureFinishOld(SimpleFeature featureFinishOld) {
        this.featureFinishOld = featureFinishOld;
    }

    /**
     *
     * @param a хранит информацию о разделении feature
     */
    public void setAdd(int a) {
        this.add = a;
    }

    public void setFeatureStartOld(SimpleFeature featureStartOld) {
        this.featureStartOld = featureStartOld;
    }

    public Guide(SimpleFeatureSource featureSource, CoordinateReferenceSystem oldCRS, CoordinateReferenceSystem newCRS, int searchType) {
        this.featureSource = featureSource;
        this.oldCRS = oldCRS;
        this.newCRS = newCRS;
        this.searchType = searchType;
    }

    public SimpleFeatureSource getFeatureSource() {
        return featureSource;
    }

    
    /**
     * Возвращает точку-проекцию на ближайшую линию на карте, подсвечивает
     * выделеную полилинию.
     * @param x
     * @param y
     * @return
     */
    public Point2D.Double getClosestPositionOnTrack(double x, double y, Geometry geom) {
        double distance = 999999999;
        Point2D.Double pointOnLine = new Point2D.Double();
        Coordinate[] line = geom.getCoordinates();

        for (int i = 0; i < line.length - 1; i++) {

            double x0 = line[i].x;
            double y0 = line[i].y;
            double x1 = line[i + 1].x;
            double y1 = line[i + 1].y;
            double x_min, y_min, x_max, y_max;
            if (x0 <= x1) {
                x_min = x0;
                x_max = x1;
            } else {
                x_min = x1;
                x_max = x0;
            }
            if (y0 <= y1) {
                y_min = y0;
                y_max = y1;
            } else {
                y_min = y1;
                y_max = y0;
            }
            if ((x <= x_max) && (x >= x_min) && (y <= y_max) && (y >= y_min)) {
                double a = y0 - y1;
                double b = x1 - x0;
                double c = x0 * y1 - x1 * y0;
                double dist = Math.abs((x * a + y * b + c) / Math.pow(a * a + b * b, 0.5));



                if ((dist < distance) && (Point2D.distance(x, y, x1, y1) >= dist) && (Point2D.distance(x, y, x0, y0) >= dist)) {
                    distance = dist;

                    double d = a * y - b * x;
                    double det = -a * a - b * b;
                    double det1 = a * c + b * d;
                    double det2 = -a * d + b * c;

                    pointOnLine.setLocation(det1 / det, det2 / det);
                }
            }
        }


        return pointOnLine;
    }

    public boolean isPointOnGeometry(double x, double y, Geometry geom) {
        double distance = 999999999;
        Point2D.Double pointOnLine = new Point2D.Double();
        Coordinate[] line = geom.getCoordinates();

        for (int i = 0; i < line.length - 1; i++) {

            double x0 = line[i].x;
            double y0 = line[i].y;
            double x1 = line[i + 1].x;
            double y1 = line[i + 1].y;
            double x_min, y_min, x_max, y_max;
            if (x0 <= x1) {
                x_min = x0;
                x_max = x1;
            } else {
                x_min = x1;
                x_max = x0;
            }
            if (y0 <= y1) {
                y_min = y0;
                y_max = y1;
            } else {
                y_min = y1;
                y_max = y0;
            }
            if ((x <= x_max) && (x >= x_min) && (y <= y_max) && (y >= y_min)) {
                double a = y0 - y1;
                double b = x1 - x0;
                double c = x0 * y1 - x1 * y0;
                double dist = Math.abs((x * a + y * b + c) / Math.pow(a * a + b * b, 0.5));



                if ((dist < distance) && (Point2D.distance(x, y, x1, y1) > dist) && (Point2D.distance(x, y, x0, y0) > dist)) {
                    distance = dist;

                    double d = a * y - b * x;
                    double det = -a * a - b * b;
                    double det1 = a * c + b * d;
                    double det2 = -a * d + b * c;

                    pointOnLine.setLocation(det1 / det, det2 / det);
                    return true;
                }
            }
        }


        return false;
    }

    /**
     * Возвращает расстояние между точкой с координатами (x, y) и
     * отрезком прямой, заданной двумя точками p1 и p2.
     * @param x координата точки
     * @param y координата точки
     * @param p1 точка прямой
     * @param p2 точка прямой
     * @return
     */
    static public double pointDistanceFromTheLine(double x, double y, DirectPosition2D p1, DirectPosition2D p2) {
        double distance = 9999999;
        double x0 = p1.x;
        double y0 = p1.y;
        double x1 = p2.x;
        double y1 = p2.y;
        double x_min, y_min, x_max, y_max;
        if (x0 <= x1) {
            x_min = x0;
            x_max = x1;
        } else {
            x_min = x1;
            x_max = x0;
        }
        if (y0 <= y1) {
            y_min = y0;
            y_max = y1;
        } else {
            y_min = y1;
            y_max = y0;
        }
        if ((x <= x_max) && (x >= x_min) && (y <= y_max) && (y >= y_min)) {
            double a = y0 - y1;
            double b = x1 - x0;
            double c = x0 * y1 - x1 * y0;
            double dist = Math.abs((x * a + y * b + c) / Math.pow(a * a + b * b, 0.5));

            if ((dist < distance) && (Point2D.distance(x, y, x1, y1) > dist) && (Point2D.distance(x, y, x0, y0) > dist)) {
                return distance = dist;
            }
        }
        return 9999999;
    }

    /**
     * Возвращает true, если точка с координатами (x, y) принадлежит
     * отрезку прямой, заданному двумя точками p1 и p2.
     * @param x координата точки
     * @param y координата точки
     * @param p1 точка прямой
     * @param p2 точка прямой
     * @return
     */
    static public boolean pointIsOnTheLine(double x, double y, DirectPosition2D p1, DirectPosition2D p2) {
        if (pointDistanceFromTheLine(x, y, p1, p2) < 0.01) {
            return true;
        }
        return false;
    }

    /**
     * Возвращает true, если точка с координатами (x, y) принадлежит
     * отрезку прямой, заданному двумя точками p1 и p2.
     * @param x координата точки
     * @param y координата точки
     * @param p1 точка прямой
     * @param p2 точка прямой
     * @return
     */
    private static boolean pointIsOnTheLine(double x, double y, Coordinate coordinate1, Coordinate coordinate2) {
        if (pointDistanceFromTheLine(x, y, new DirectPosition2D(coordinate1.x, coordinate1.y), new DirectPosition2D(coordinate2.x, coordinate2.y)) < 0.1) {
            return true;
        }
        return false;
    }

    /**
     * Функция разбивает feature на две части точкой point. Производится проверка
     * принадлежности точки к feature.
     * @param point
     * @param feature
     * @return
     */
    static public SimpleFeature[] splitFeature(DirectPosition2D point, SimpleFeature feature) {
        double x = point.x;
        double y = point.y;
        SimpleFeature duplicate = (SimpleFeature) DataUtilities.duplicate(feature);
        SimpleFeature duplicate1 = (SimpleFeature) DataUtilities.duplicate(feature);
        Geometry geom = (MultiLineString) duplicate.getDefaultGeometry();
        Coordinate[] points = geom.getCoordinates();
        int pointNumber = -1; //точка клика лежит в списке после pointNumber.
        for (int i = 0; i < points.length - 1; i++) {
            if (pointIsOnTheLine(point.x, point.y, points[i], points[i + 1])) {
                pointNumber = i;
            }
        }

        if (pointNumber == -1) {
            return null;
        }
        Coordinate[] points1 = new Coordinate[pointNumber + 2];
        Coordinate[] points2 = new Coordinate[points.length - pointNumber];
        System.arraycopy(points, 0, points1, 0, pointNumber + 1);
        points1[pointNumber + 1] = new Coordinate();
        points1[pointNumber + 1].x = x;
        points1[pointNumber + 1].y = y;
        System.arraycopy(points, pointNumber + 1, points2, 1, points.length - pointNumber - 1);
        points2[0] = new Coordinate();
        points2[0].x = x;
        points2[0].y = y;
        GeometryFactory geometryFactory = JTSFactoryFinder.getGeometryFactory(null);
        LineString line = geometryFactory.createLineString(points1);
        LineString line1 = geometryFactory.createLineString(points2);
        duplicate.setDefaultGeometry(line);
        duplicate1.setDefaultGeometry(line1);
        Geometry geom1 = (MultiLineString) duplicate.getDefaultGeometry();
        Geometry geom2 = (MultiLineString) duplicate1.getDefaultGeometry();
        SimpleFeature[] RETURN = new SimpleFeature[2];
        RETURN[0] = duplicate;
        RETURN[1] = duplicate1;
        return RETURN;
    }

    SimpleFeatureCollection distance(MapMouseEvent ev) throws Exception {
        DirectPosition2D worldPosition = ev.getMapPosition();

        // get the unit of measurement
        SimpleFeatureType schema = featureSource.getSchema();
        CoordinateReferenceSystem crs = schema.getGeometryDescriptor().getCoordinateReferenceSystem();
        Unit<?> uom = crs.getCoordinateSystem().getAxis(0).getUnit();

        MathTransform transform = CRS.findMathTransform(worldPosition.getCoordinateReferenceSystem(),
                crs, true);

        DirectPosition dataPosition = transform.transform(worldPosition, null);

        com.vividsolutions.jts.geom.Point point = JTS.toGeometry(dataPosition);

        // threshold distance
        double distance = 10.0d;

        FilterFactory2 ff = CommonFactoryFinder.getFilterFactory2(null);
        Filter filter = ff.dwithin(ff.property("POLYGON"), ff.literal(point), distance, uom.toString());

        return featureSource.getFeatures(filter);
    }

    /**
     * Функция является wrapper к функции routineAlgorithm. RoutineAlgorithm
     * в качестве аргументов использует переменные класса Guide. Чтобы
     * упростить работу с классом написан метод guidance.
     * @param start
     * @param finish
     * @param idStart
     * @param idFinish
     * @return
     */
    public Path guidance(Point2D start, Point2D finish, long idStart, long idFinish) {
        try {
            //featureSource.getFeatures копирует данные в память
            SimpleFeatureCollection features = featureSource.getFeatures(CQL.toFilter("ID = '" + idStart + "'"));
            Object[] toArray = features.toArray();
            if (toArray.length == 0) {
                System.out.println("массив 1 пустой");
                return null;
            }
            SimpleFeature featureStart = (SimpleFeature) toArray[0];
            SimpleFeatureCollection features1 = featureSource.getFeatures(CQL.toFilter("ID = '" + idFinish + "'"));
            Object[] toArray1 = features1.toArray();
            if (toArray1.length == 0) {
                System.out.println("массив 2 пустой");
                return null;
            }
            SimpleFeature featureFinish = (SimpleFeature) toArray1[0];

            //from guideButton
            setFeatureStartOld(featureStart);
            setSimpleFeatureStart(splitFeature(new DirectPosition2D(start), featureStart));

            DirectPosition2D finishPoint = new DirectPosition2D(finish);
            setFeatureFinishOld(featureFinish);
            if (getFeatureStartOld().equals(featureFinish)) {
                if (isPointOnGeometry(finishPoint.x, finishPoint.y, (MultiLineString) getSimpleFeatureStart()[0].getDefaultGeometry())) {
                    setSimpleFeatureFinish(splitFeature(finishPoint, getSimpleFeatureStart()[0]));
                    if (isPointOnGeometry(finishPoint.x, finishPoint.y, (MultiLineString) getSimpleFeatureFinish()[0].getDefaultGeometry())) {
                        setAdd(1);
                    } else {
                        setAdd(2);
                    }
                } else if (isPointOnGeometry(finishPoint.x, finishPoint.y, (MultiLineString) getSimpleFeatureStart()[1].getDefaultGeometry())) {
                    setSimpleFeatureFinish(splitFeature(finishPoint, getSimpleFeatureStart()[1]));
                    if (isPointOnGeometry(finishPoint.x, finishPoint.y, (MultiLineString) getSimpleFeatureFinish()[0].getDefaultGeometry())) {
                        setAdd(3);
                    } else {
                        setAdd(4);
                    }
                }
            } else {
                setSimpleFeatureFinish(splitFeature(finishPoint, featureFinish));
            }


        } catch (CQLException ex) {
            Logger.getLogger(Guide.class.getName()).log(Level.SEVERE, null, ex);
        } catch (IOException ex) {
            Logger.getLogger(Guide.class.getName()).log(Level.SEVERE, null, ex);
        }
        return routineAlgorithm();
    }

    /**
     * Функция поиска кратчайшего пути.
     * Алгоритм собирает граф из featureSource, simpleFeatureStart[2], simpleFeatureFinish[2]
     * исключая исходные featureStartOld и featureFinishOld.
     *
     * @return Возвращает путь.
     */
    public Path routineAlgorithm() {
        try {
            // get a feature collection somehow
            SimpleFeatureCollection fCollection = featureSource.getFeatures();
            //create a linear graph generate
            DirectedLineStringGraphGenerator lineStringGen = new DirectedLineStringGraphGenerator();
            //wrap it in a feature graph generator
            MyFeatureGraphGenerator featureGen = new MyFeatureGraphGenerator(lineStringGen);
            //throw all the features into the graph generator
            FeatureIterator iter = fCollection.features();

            /**
             * Автор: Медведь
             * Описание: в случае, если точки старта и финиша лежат на одной дуге,
             * необходимо провести разбиение дуги на три части.
             */
            try {
                while (iter.hasNext()) {
                    Feature feature = iter.next();
                    if (feature.equals(featureFinishOld)) {
                        if (add != 0) {
                            if (add == 1) {
                                featureGen.add(simpleFeatureStart[1]);
                                for (int i = 0; i < simpleFeatureFinish.length; i++) {
                                    featureGen.add(simpleFeatureFinish[i]);
                                }
                                simpleFeatureStart[0] = simpleFeatureFinish[0];
                            } else if (add == 2) {
                                featureGen.add(simpleFeatureStart[1]);
                                for (int i = 0; i < simpleFeatureFinish.length; i++) {
                                    featureGen.add(simpleFeatureFinish[i]);
                                }
                                simpleFeatureStart[0] = simpleFeatureFinish[1];
                            } else if (add == 3) {
                                featureGen.add(simpleFeatureStart[0]);
                                for (int i = 0; i < simpleFeatureFinish.length; i++) {
                                    featureGen.add(simpleFeatureFinish[i]);
                                }
                                simpleFeatureStart[1] = simpleFeatureFinish[1];
                            } else if (add == 4) {
                                featureGen.add(simpleFeatureStart[0]);
                                for (int i = 0; i < simpleFeatureFinish.length; i++) {
                                    featureGen.add(simpleFeatureFinish[i]);
                                }
                                simpleFeatureStart[1] = simpleFeatureFinish[0];
                            }
                        } else {
                            for (int i = 0; i < simpleFeatureFinish.length; i++) {
                                featureGen.add(simpleFeatureFinish[i]);

                            }
                        }
                    } else if (feature.equals(featureStartOld)) {
                        for (int i = 0; i < simpleFeatureStart.length; i++) {
                            featureGen.add(simpleFeatureStart[i]);
                        }
                    } else {
                        featureGen.add(feature);
                    }
                }
            } finally {
                iter.close();
            }

            BasicDirectedGraph graph = (BasicDirectedGraph) featureGen.getGraph();
            //find a source node (usually your user chooses one)
            Collection nodes = graph.getNodes();

            Collection edges = graph.getEdges();

            Node leftNode1 = null;
            Node rightNode1 = null;
            Node leftNode2 = null;
            Node rightNode2 = null;

            Node finishLeftNode1 = null;
            Node finishRightNode1 = null;
            Node finishLeftNode2 = null;
            Node finishRightNode2 = null;

            boolean founded2[] = new boolean[4];
            for (Iterator e = edges.iterator(); e.hasNext();) {
                Edge edge = (Edge) e.next();
                SimpleFeature feature = (SimpleFeature) edge.getObject();
                if (feature.equals(getSimpleFeatureStart()[0])) {
                    leftNode1 = edge.getNodeA();
                    rightNode1 = edge.getNodeB();
                    founded2[0] = true;
                    if (founded2[0] && founded2[1] && founded2[2] && founded2[3]) {
                        break;
                    }
                } else if (feature.equals(getSimpleFeatureStart()[1])) {
                    leftNode2 = edge.getNodeA();
                    rightNode2 = edge.getNodeB();
                    founded2[1] = true;
                    if (founded2[0] && founded2[1] && founded2[2] && founded2[3]) {
                        break;
                    }
                }
                if (feature.equals(getSimpleFeatureFinish()[0])) {
                    finishLeftNode1 = edge.getNodeA();
                    finishRightNode1 = edge.getNodeB();
                    founded2[2] = true;
                    if (founded2[0] && founded2[1] && founded2[2] && founded2[3]) {
                        break;
                    }
                } else if (feature.equals(getSimpleFeatureFinish()[1])) {
                    finishLeftNode2 = edge.getNodeA();
                    finishRightNode2 = edge.getNodeB();
                    founded2[3] = true;
                    if (founded2[0] && founded2[1] && founded2[2] && founded2[3]) {
                        break;
                    }
                }
            }
            if ((founded2[0] && founded2[1] && founded2[2] && founded2[3]) == false) {
                return null;
            }

            Node start = null;
            Node finish = null;

            if (leftNode1.equals(leftNode2) || leftNode1.equals(rightNode2)) {
                start = leftNode1;
            } else if (leftNode2.equals(rightNode1)) {
                start = leftNode2;
            } else if (rightNode1.equals(rightNode2)) {
                start = rightNode1;
            }

            if (finishLeftNode1.equals(finishLeftNode2) || finishLeftNode1.equals(finishRightNode2)) {
                finish = finishLeftNode1;
            } else if (finishLeftNode2.equals(finishRightNode1)) {
                finish = finishLeftNode2;
            } else if (finishRightNode1.equals(finishRightNode2)) {
                finish = finishRightNode1;
            }



// create a strategy for weighting edges in the graph
// in this case we are using geometry length

            DirectedDijkstraIterator.EdgeWeighter weighter = new DijkstraIterator.EdgeWeighter() {

                public double getWeight(Edge e) {
                    SimpleFeature feature = (SimpleFeature) e.getObject();
                    Geometry geometry = (Geometry) feature.getDefaultGeometry();
                    return geometry.getLength();
                }
            };

// Create GraphWalker - in this case DijkstraShortestPathFinder
            if (searchType == 0) {
                DirectedDijkstraShortestPathFinder ddspf = new DirectedDijkstraShortestPathFinder(graph, start, weighter);
                ddspf.calculate();
                return ddspf.getPath(finish);
            } else if (searchType > 0) {
                MySearch ms = new MySearch(searchType);
                return ms.search(graph, start, finish);
            }
        } catch (IOException ex) {
            Logger.getLogger(MyGraph.class.getName()).log(Level.SEVERE, null, ex);
        }
        return null;
    }

}